package jblip.base;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import jblip.BlipApi;
import jblip.BlipClient;
import jblip.ErrorMessage;
import jblip.resources.Media;
import jblip.resources.Shortlink;
import jblip.resources.Subscription;
import jblip.resources.Update;
import jblip.resources.User;
import jblip.resources.UserPicture;
import jblip.resources.Media.MediaType;
import jblip.util.Base64Encoder;
import jblip.util.HttpConnectionProvider;
import jblip.util.MultipartData;
import jblip.util.json.JSONArray;
import jblip.util.json.JSONParser;

public class HttpClient extends BlipClient {

  private final static String LIB_USER_AGENT = "JBlip 0.2-dev";

  private BlipApi used_api;

  private ErrorMessage errmsg;

  protected final HttpConnectionProvider connection_provider;

  public HttpClient(final String username, final String password) {
    this("api.blip.pl", username, password);
  }

  public HttpClient(final String host, final String username,
      final String password) {
    super(host, username, password);
    this.used_api = BlipApi.API002;
    this.connection_provider = new HttpConnectionProvider(8);
  }

  protected HttpURLConnection getConnection(String target, boolean auth)
      throws IOException {
    URL url = new URL("http", this.host_, target);
    System.err.println("Request: " + url.toString());

    HttpURLConnection conn = connection_provider.getConnection(url);
    conn.setRequestProperty("Accept", "application/json");
    conn.setRequestProperty("User-Agent", LIB_USER_AGENT);
    conn.setRequestProperty("X-Blip-API", used_api.toString());
    conn.setRequestProperty("Host", this.host_);

    if (auth) {
      byte[] credentials = (user_ + ":" + pass_).getBytes("UTF-8");
      conn.setRequestProperty("Authorization", "Basic "
          + Base64Encoder.encode(credentials));
    }

    conn.setConnectTimeout(15000);
    conn.setReadTimeout(15000);

    return conn;
  }

  protected InputStream getObjectStream(final String target, final boolean auth)
      throws IOException {
    final HttpURLConnection conn = getConnection(target, auth);
    conn.setRequestMethod("GET");
    conn.connect();
    final int resp = conn.getResponseCode();
    // TODO: refactor to release connection only after stream is not used
    connection_provider.releaseConnection(conn);
    if (resp == 200) {
      return conn.getInputStream();
    } else {
      errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      return null;
    }
  }

  private void deleteResource(final String path) {
    errmsg = null;
    HttpURLConnection conn = null;
    try {
      conn = getConnection(path, true);
      conn.setRequestMethod("DELETE");
      conn.connect();

      int resp = conn.getResponseCode();
      if (resp != 200) {
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
      connection_provider.releaseConnection(conn);
    } catch (IOException e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void setSubscription(final String user, final boolean transport_www,
      final boolean transport_im) {
    if (user == null || user.equals("")) {
      throw new IllegalArgumentException("Invalid user (null or empty)");
    }
    errmsg = null;
    StringBuilder url_builder = new StringBuilder(64);
    url_builder.append("/subscriptions/");
    url_builder.append(user);
    url_builder.append("?subscription%5Bwww%5D=");
    url_builder.append(transport_www ? '1' : '0');
    url_builder.append("&subscription%5Bim%5D=");
    url_builder.append(transport_im ? '1' : '0');

    HttpURLConnection conn = null;
    try {
      conn = getConnection(url_builder.toString(), true);
      conn.setRequestMethod("PUT");
      // we need this because nginx forces it
      conn.setRequestProperty("Content-Length", "0");
      conn.connect();

      int resp = conn.getResponseCode();
      if (resp != 200) {
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
      connection_provider.releaseConnection(conn);
    } catch (IOException e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void deleteSubscriptions(final String user) {
    if (user == null || user.equals("")) {
      throw new IllegalArgumentException("Invalid user (null or empty)");
    }
    deleteResource("/subscriptions/" + user);
  }

  private Iterable<? extends Subscription> getSubscriptionsByURL(String url) {
    errmsg = null;
    try {
      InputStream input = getObjectStream(url, true);
      if (input != null) {
        JSONParser parser = new JSONParser();
        JSONArray object_array = (JSONArray) parser.parseStream(input);
        List<Subscription> subs_list =
            new ArrayList<Subscription>(object_array.length());
        for (int i = 0; i < object_array.length(); i++) {
          subs_list.add(resource_factory.getSubscription(object_array.get(i)));
        }
        return subs_list;
      } else {
        return null;
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
      return null;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Iterable<? extends Subscription> getSubscriptions(final String user) {
    String url = (user == null ? "" : ("/users/" + user)) + "/subscriptions";
    return getSubscriptionsByURL(url);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Iterable<? extends Subscription> getSubscriptionsFrom(
      final String user) {
    String url =
        (user == null ? "" : ("/users/" + user)) + "/subscriptions/from";
    return getSubscriptionsByURL(url);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Iterable<? extends Subscription> getSubscriptionsTo(String user) {
    String url = (user == null ? "" : ("/users/" + user)) + "/subscriptions/to";
    return getSubscriptionsByURL(url);
  }

  @Override
  public void deleteMessage(Integer id) {
    if (id == null || id <= 0) {
      throw new IllegalArgumentException("ID is either null or not positive");
    }
    deleteResource("/updates/" + id);
  }

  @Override
  public Update getMessage(final int id) {
    errmsg = null;

    try {
      final InputStream input = getObjectStream("/updates/" + id, true);
      if (input != null) {
        return resource_factory.getUpdate(new JSONParser().parseStream(input));
      } else {
        return null;
      }

    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
      return null;
    }
  }

  @Override
  public Media getMovie(Integer update) {
    return getMedia("/updates/" + update + "/movie", MediaType.MOVIE);
  }

  @Override
  public Media getRecording(Integer update) {
    return getMedia("/updates/" + update + "/recording", MediaType.RECORDING);
  }

  private Iterable<? extends Media> getPicturesList(final JSONArray array) {
    final List<Media> pics = new ArrayList<Media>(array.length());
    for (int i = 0; i < array.length(); i++) {
      pics.add(resource_factory.getMedia(array.get(i), MediaType.PICTURE));
    }
    return pics;
  }

  @Override
  public Iterable<? extends Media> getPictures(final Integer update) {
    errmsg = null;
    try {
      final InputStream input =
          getObjectStream("/updates/" + update + "/pictures", true);
      if (input != null) {
        final JSONArray array = (JSONArray) new JSONParser().parseStream(input);
        return getPicturesList(array);
      } else {
        return null;
      }
    } catch (IOException e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
      return null;
    }
  }

  @Override
  public UserPicture getAvatar(final String user) {
    return getUserPicture("/users/" + user + "/avatar");
  }

  @Override
  public UserPicture getAvatar(final Integer user_id) {
    return getUserPicture("/users/" + user_id + "/avatar");
  }

  @Override
  public void deleteAvatar() {
    deleteResource("/avatar");
  }

  @Override
  public Media getBackground(final String user) {
    return getMedia("/users/" + user + "/background", MediaType.PICTURE);
  }

  @Override
  public Media getBackground(final Integer user_id) {
    return getMedia("/users/" + user_id + "/background", MediaType.PICTURE);
  }

  @Override
  public void deleteBackground() {
    deleteResource("/background");
  }

  private Media getMedia(String path, MediaType type) {
    errmsg = null;
    try {
      final InputStream input = getObjectStream(path, true);
      if (input != null) {
        return resource_factory.getMedia(
            new JSONParser().parseStream(input), type);
      } else {
        return null;
      }
    } catch (IOException e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    }
    return null;
  }

  private UserPicture getUserPicture(final String path) {
    errmsg = null;
    try {
      final InputStream input = getObjectStream(path, true);
      if (input != null) {
        return resource_factory.getUserPicture(new JSONParser()
            .parseStream(input));
      } else {
        return null;
      }
    } catch (IOException e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    }
    return null;

  }

  @Override
  public User getUser(int id) {
    return getUser(String.valueOf(id));
  }

  @Override
  public User getUser(String login) {
    errmsg = null;

    try {
      InputStream input;
      if (login == null) {
        input = getObjectStream("/users/" + this.user_, true);
      } else {
        input = getObjectStream("/users/" + login, false);
      }

      if (input != null) {
        return resource_factory.getUser(new JSONParser().parseStream(input));
      } else {
        return null;
      }

    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
      return null;
    }
  }

  private Iterable<Update> poll(final String address) {
    errmsg = null;
    HttpURLConnection conn = null;
    try {
      conn = getConnection(address, true);
      conn.setRequestMethod("GET");
      conn.connect();

      int resp = conn.getResponseCode();

      if (resp == 204) { // 'No content'
        return Collections.emptyList();
      }

      if (resp == 200) {
        JSONParser parser = new JSONParser();
        JSONArray object_array =
            (JSONArray) parser.parseStream(conn.getInputStream());
        List<Update> update_list = new ArrayList<Update>();
        for (int i = 0; i < object_array.length(); i++) {
          update_list.add(resource_factory.getUpdate(object_array.get(i)));
        }

        return update_list;

      } else {
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
        return null;
      }

    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
      return null;
    } finally {
      connection_provider.releaseConnection(conn);
    }

  }

  private String getPollAddress(final String category, final String uname,
      final Integer since, final Integer limit, final Integer offset,
      final boolean all) {
    verifyPollParams(category, since, limit, offset);

    StringBuilder address = new StringBuilder(32);

    if (uname != null) {
      address.append("/users/");
      address.append(uname);
    }

    address.append('/');
    address.append(category);

    if (all && since == null) {
      address.append("/all");
    }

    if (since != null) {
      address.append('/');
      address.append(since);
      address.append(all ? "/all_since" : "/since");
    }

    if (limit != null) {
      address.append("?limit=");
      address.append(limit);
    }

    if (offset != null) {
      address.append(limit == null ? '?' : '&');
      address.append("offset=");
      address.append(offset);
    }

    return address.toString();
  }

  private String getReversePollAddress(final String category,
      final String uname, final Integer since, final Integer limit,
      final Integer offset, final boolean all) {
    verifyPollParams(category, since, limit, offset);

    final StringBuilder address = new StringBuilder(32);

    if (uname != null) {
      address.append("/users/");
      address.append(uname);
    }

    address.append('/');
    address.append(category);

    if (all && since == null) {
      address.append("/all");
    }

    if (since != null) {
      address.append(all ? "/all_since" : "/since");
      address.append('/');
      address.append(since);
    }

    if (limit != null) {
      address.append("?limit=");
      address.append(limit);
    }

    if (offset != null) {
      address.append(limit == null ? '?' : '&');
      address.append("offset=");
      address.append(offset);
    }

    return address.toString();
  }

  private String getPollAddress(String uname, Integer since, Integer limit,
      Integer offset, boolean all) {
    return getPollAddress("updates", uname, since, limit, offset, all);
  }

  private void verifyPollParams(final String category, final Integer since,
      final Integer limit, final Integer offset) {
    if (since != null && since <= 0) {
      throw new IllegalArgumentException("'since' parameter must be positive");
    }

    if (limit != null && limit <= 0) {
      throw new IllegalArgumentException("'limit' parameter must be positive");
    }

    if (offset != null && offset < 0) {
      throw new IllegalArgumentException(
          "'offset' parameter must be non-negative");
    }

    if (category == null) {
      throw new IllegalArgumentException("'category' can't be null");
    }
  }

  @Override
  public Iterable<? extends Update> poll(final Integer since,
      final Integer limit, final Integer offset) {
    String address = getPollAddress(null, since, limit, offset, false);
    return poll(address);
  }

  @Override
  public Iterable<? extends Update> poll(final String username,
      final Integer since, final Integer limit, final Integer offset) {
    String address = getPollAddress(username, since, limit, offset, false);
    return poll(address);
  }

  @Override
  public Iterable<? extends Update> pollAll(final Integer since,
      final Integer limit, final Integer offset) {
    String address = getPollAddress(null, since, limit, offset, true);
    return poll(address);
  }

  @Override
  public Iterable<? extends Update> pollBliposhere(final Integer since,
      final Integer limit, final Integer offset) {
    final String address =
        getReversePollAddress("bliposphere", null, since, limit, offset, false);
    return poll(address);
  }

  @Override
  public Iterable<? extends Update> pollDashboard(final Integer since,
      final Integer limit, final Integer offset) {
    final String address =
        getReversePollAddress("dashboard", null, since, limit, offset, false);
    return poll(address);
  }

  @Override
  public Iterable<? extends Update> pollDashboard(final String username,
      final Integer since, final Integer limit, final Integer offset) {
    final String address =
        getReversePollAddress(
            "dashboard", username, since, limit, offset, false);
    return poll(address);
  }

  @Override
  public Iterable<? extends Media> pollPictures(final Integer since,
      final Integer limit, final Integer offset) {
    final String address =
        getPollAddress("pictures", null, since, limit, offset, true);
    HttpURLConnection conn = null;
    try {
      conn = getConnection(address, true);
      conn.setRequestMethod("GET");
      conn.connect();

      int resp = conn.getResponseCode();

      if (resp == 204) { // 'No content'
        return Collections.emptyList();
      }

      if (resp == 200) {
        JSONParser parser = new JSONParser();
        JSONArray array = (JSONArray) parser.parseStream(conn.getInputStream());
        return getPicturesList(array);
      } else {
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
        return null;
      }

    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
      return null;
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public Iterable<? extends Shortlink> pollShortlinks(final Integer since,
      final Integer limit, final Integer offset) {
    errmsg = null;
    final String address =
        getPollAddress("shortlinks", null, since, limit, offset, true);
    HttpURLConnection conn = null;
    try {
      conn = getConnection(address, true);
      conn.setRequestMethod("GET");
      conn.connect();

      int resp = conn.getResponseCode();

      if (resp == 204) { // 'No content'
        return Collections.emptyList();
      }

      if (resp == 200) {
        JSONArray object_array =
            (JSONArray) new JSONParser().parseStream(conn.getInputStream());
        List<Shortlink> update_list = new ArrayList<Shortlink>();
        for (int i = 0; i < object_array.length(); i++) {
          update_list.add(resource_factory.getShortlink(object_array.get(i)));
        }

        return update_list;
      } else {
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
        return null;
      }

    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
      return null;
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public Iterable<? extends Update> pollTag(final String tag,
      final Integer since, final Integer limit, final Integer offset) {
    final String address =
        getReversePollAddress("tags/" + tag, null, since, limit, offset, false);
    return poll(address);
  }

  private int postMessage(final HttpURLConnection conn, final String body)
      throws IOException {
    final Integer content_length = body.getBytes("UTF-8").length;
    conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
    conn.setRequestProperty("Content-Length", content_length.toString());
    conn.setDoInput(true);
    conn.setDoOutput(true);
    conn.setRequestMethod("POST");
    conn.connect();
    OutputStreamWriter osw = new OutputStreamWriter(conn.getOutputStream());
    osw.write(body);
    osw.close();
    return conn.getResponseCode();
  }

  private int postMessage(final HttpURLConnection conn, final MultipartData data)
      throws IOException {
    conn.setRequestProperty("Content-Type", data.getContentType());
    conn.setDoInput(true);
    conn.setDoOutput(true);
    conn.setRequestMethod("POST");
    conn.connect();

    final OutputStream output = conn.getOutputStream();
    data.writeToStream(output);
    output.close();

    return conn.getResponseCode();
  }

  @Override
  public void postMessage(final String body) {
    errmsg = null;
    HttpURLConnection conn = null;
    try {
      conn = getConnection("/updates", true);
      String encoded_body = "update[body]=" + URLEncoder.encode(body, "UTF-8");
      int resp = postMessage(conn, encoded_body);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postMessage(final String body, final String recipient) {
    errmsg = null;

    HttpURLConnection conn = null;
    try {
      conn = getConnection("/directed_messages", true);
      String encoded_body =
          "directed_message[recipient]="
              + URLEncoder.encode(recipient, "UTF-8") + "&"
              + "directed_message[body]=" + URLEncoder.encode(body, "UTF-8");
      int resp = postMessage(conn, encoded_body);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postMessage(final String body, final InputStream picture) {
    errmsg = null;
    HttpURLConnection conn = null;
    try {
      conn = getConnection("/updates", true);
      final MultipartData data = new MultipartData();
      data.addText("update[body]", body);
      data.addStreamData("update[picture]", picture);

      final int resp = postMessage(conn, data);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postMessage(final String body, final String recipient,
      final InputStream picture) {
    errmsg = null;

    HttpURLConnection conn = null;
    try {
      conn = getConnection("/directed_messages", true);
      final MultipartData data = new MultipartData();
      data.addText("directed_message[body]", body);
      data.addText("directed_message[recipient]", recipient);
      data.addStreamData("directed_message[picture]", picture);

      final int resp = postMessage(conn, data);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postMessage(final String body, final File picture) {
    errmsg = null;

    HttpURLConnection conn = null;
    try {
      conn = getConnection("/updates", true);
      final MultipartData data = new MultipartData();
      data.addText("update[body]", body);
      data.addFileData("update[picture]", picture);

      final int resp = postMessage(conn, data);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postMessage(final String body, final String recipient,
      final File picture) {
    errmsg = null;

    HttpURLConnection conn = null;
    try {
      conn = getConnection("/directed_messages", true);
      final MultipartData data = new MultipartData();
      data.addText("directed_message[body]", body);
      data.addText("directed_message[recipient]", recipient);
      data.addFileData("directed_message[picture]", picture);

      final int resp = postMessage(conn, data);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postPrivateMessage(final String body, final String recipient) {
    errmsg = null;

    HttpURLConnection conn = null;
    try {
      conn = getConnection("/private_messages", true);
      String encoded_body =
          "private_message[recipient]=" + URLEncoder.encode(recipient, "UTF-8")
              + "&" + "private_message[body]="
              + URLEncoder.encode(body, "UTF-8");
      int resp = postMessage(conn, encoded_body);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postPrivateMessage(final String body, final String recipient,
      final InputStream picture) {
    errmsg = null;

    HttpURLConnection conn = null;
    try {
      conn = getConnection("/private_messages", true);
      final MultipartData data = new MultipartData();
      data.addText("private_message[body]", body);
      data.addText("private_message[recipient]", recipient);
      data.addStreamData("private_message[picture]", picture);

      final int resp = postMessage(conn, data);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }
  }

  @Override
  public void postPrivateMessage(final String body, final String recipient,
      final File picture) {
    errmsg = null;

    HttpURLConnection conn = null;    
    try {
      conn = getConnection("/private_messages", true);
      final MultipartData data = new MultipartData();
      data.addText("private_message[body]", body);
      data.addText("private_message[recipient]", recipient);
      data.addFileData("private_message[picture]", picture);

      final int resp = postMessage(conn, data);

      if (resp != 201) { // 'Created'
        errmsg = ErrorMessage.getHTTPErrorMessage(resp);
      }
    } catch (Exception e) {
      errmsg = ErrorMessage.getExceptionMessage(e);
    } finally {
      connection_provider.releaseConnection(conn);
    }

  }

  @Override
  public ErrorMessage getLastError() {
    return errmsg;
  }

  @Override
  public BlipApi getSupportedAPI() {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public synchronized boolean testLogin() {
    HttpURLConnection conn = null;
    try {
      conn = getConnection("/login", true);
      conn.setRequestMethod("HEAD");
      final int resp = conn.getResponseCode();
      if (resp == 200) {
        return true;
      } else if (resp != 401) {
        System.err.println("Unexpected test login response: " + resp);
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      connection_provider.releaseConnection(conn);
    }
    return false;
  }
}
